﻿using JESAI.DistributedLock.Internal;
using JESAI.DistributedLock.Redis.RedLock;
using StackExchange.Redis;
using System.Threading.Tasks;

namespace JESAI.DistributedLock.Redis.Primitives;

/// <summary>
/// Redis读取锁原语
/// </summary>
internal class RedisReadLockPrimitive : IRedLockAcquirableSynchronizationPrimitive, IRedLockExtensibleSynchronizationPrimitive
{
    private readonly RedisValue _lockId = RedLockHelper.CreateLockId();
    private readonly RedisKey _readerKey, _writerKey;
    private readonly RedLockTimeouts _timeouts;

    public RedisReadLockPrimitive(RedisKey readerKey, RedisKey writerKey, RedLockTimeouts timeouts)
    {
        this._readerKey = readerKey;
        this._writerKey = writerKey;
        this._timeouts = timeouts;
    }

    public TimeoutValue AcquireTimeout => this._timeouts.AcquireTimeout;

    /// <summary>
    /// 发布阅读
    /// 
    /// 只需从阅读器集中删除我们的ID（如果没有ID或DNE集，则不执行）
    /// </summary>
    private static readonly RedisScript<RedisReadLockPrimitive> ReleaseReadScript = new(
        @"redis.call('srem', @readerKey, @lockId)",
        p => new { readerKey = p._readerKey, lockId = p._lockId }
    );

    public void Release(IDatabase database, bool fireAndForget) => ReleaseReadScript.Execute(database, this, fireAndForget);
    public Task ReleaseAsync(IDatabaseAsync database, bool fireAndForget) => ReleaseReadScript.ExecuteAsync(database, this, fireAndForget);

    /// <summary>
    /// 尝试扩展阅读
    /// 
    /// 首先，检查阅读器集是否存在，我们的ID是否仍然是成员。否则，我们就会失败。
    /// 
    /// 然后，将读卡器设置TTL扩展为至少是我们的到期日（至少因为其他读卡器可能正在使用更长的到期日）
    /// </summary>
    private static readonly RedisScript<RedisReadLockPrimitive> TryExtendReadScript = new(@"
            if redis.call('sismember', @readerKey, @lockId) == 0 then
                return 0
            end
            if redis.call('pttl', @readerKey) < tonumber(@expiryMillis) then
                redis.call('pexpire', @readerKey, @expiryMillis)
            end
            return 1",
        p => new { readerKey = p._readerKey, lockId = p._lockId, expiryMillis = p._timeouts.Expiry.InMilliseconds }
    );

    public Task<bool> TryExtendAsync(IDatabaseAsync database) => TryExtendReadScript.ExecuteAsync(database, this).AsBooleanTask();

    /// <summary>
    /// 尝试获取读取
    /// 
    /// 首先，检查writer锁值：如果存在，则失败。
    /// 
    /// 然后，将我们的ID添加到阅读器集中，如果它不存在，则创建它。然后，将阅读器的TTL设置至少延长到我们的到期时间。返回成功。
    /// </summary>
    private static readonly RedisScript<RedisReadLockPrimitive> TryAcquireReadScript = new($@"
            if redis.call('exists', @writerKey) == 1 then
                return 0
            end
            redis.call('sadd', @readerKey, @lockId)
            local readerTtl = redis.call('pttl', @readerKey)
            if readerTtl < tonumber(@expiryMillis) then
                redis.call('pexpire', @readerKey, @expiryMillis)
            end
            return 1",
        p => new { writerKey = p._writerKey, readerKey = p._readerKey, lockId = p._lockId, expiryMillis = p._timeouts.Expiry.InMilliseconds }
    );

    public Task<bool> TryAcquireAsync(IDatabaseAsync database) => TryAcquireReadScript.ExecuteAsync(database, this).AsBooleanTask();
    public bool TryAcquire(IDatabase database) => (bool)TryAcquireReadScript.Execute(database, this);

    public bool IsConnected(IDatabase database) => database.IsConnected(this._readerKey, CommandFlags.DemandMaster);
}

internal class RedisWriterWaitingPrimitive : RedisMutexPrimitive
{
    public const string LockIdSuffix = "_WRITERWAITING";

    public RedisWriterWaitingPrimitive(RedisKey writerKey, RedisValue baseLockId, RedLockTimeouts timeouts)
        : base(writerKey, baseLockId + LockIdSuffix, timeouts)
    {
    }
}

internal class RedisWriteLockPrimitive : IRedLockAcquirableSynchronizationPrimitive, IRedLockExtensibleSynchronizationPrimitive
{
    private readonly RedisKey _readerKey, _writerKey;
    private readonly RedisValue _lockId;
    private readonly RedLockTimeouts _timeouts;
    private readonly RedisMutexPrimitive _mutexPrimitive;

    public RedisWriteLockPrimitive(
        RedisKey readerKey,
        RedisKey writerKey,
        RedisValue lockId,
        RedLockTimeouts timeouts)
    {
        this._readerKey = readerKey;
        this._writerKey = writerKey;
        this._lockId = lockId;
        this._timeouts = timeouts;
        this._mutexPrimitive = new RedisMutexPrimitive(this._writerKey, this._lockId, this._timeouts);
    }

    public TimeoutValue AcquireTimeout => this._timeouts.AcquireTimeout;

    public void Release(IDatabase database, bool fireAndForget) => this._mutexPrimitive.Release(database, fireAndForget);
    public Task ReleaseAsync(IDatabaseAsync database, bool fireAndForget) => this._mutexPrimitive.ReleaseAsync(database, fireAndForget);

    /// <summary>
    /// 尝试获取写入
    /// 
    ///  首先，检查writerValue是否存在。如果是这样，除非是我们等待的ID，否则失败。
    /// 
    /// 然后，检查是否没有阅读器。如果是这样，则将writerValue设置为我们的ID并返回成功。如果没有，那么如果锁有我们的等待ID，则重新过期（避免了延长写入器等待锁的需要）。
    /// 
    /// 最后，返回失败。
    /// </summary>
    private static readonly RedisScript<RedisWriteLockPrimitive> TryAcquireWriteScript = new($@"
            local writerValue = redis.call('get', @writerKey)
            if writerValue == false or writerValue == @lockId .. '{RedisWriterWaitingPrimitive.LockIdSuffix}' then
                if redis.call('scard', @readerKey) == 0 then
                    redis.call('set', @writerKey, @lockId, 'px', @expiryMillis)
                    return 1
                end
                if writerValue ~= false then
                    redis.call('pexpire', @writerKey, @expiryMillis)
                end
            end
            return 0",
        p => new { writerKey = p._writerKey, readerKey = p._readerKey, lockId = p._lockId, expiryMillis = p._timeouts.Expiry.InMilliseconds }
    );

    public bool TryAcquire(IDatabase database) => (bool)TryAcquireWriteScript.Execute(database, this);
    public Task<bool> TryAcquireAsync(IDatabaseAsync database) => TryAcquireWriteScript.ExecuteAsync(database, this).AsBooleanTask();

    public Task<bool> TryExtendAsync(IDatabaseAsync database) => this._mutexPrimitive.TryExtendAsync(database);

    public bool IsConnected(IDatabase database) => database.IsConnected(this._writerKey, CommandFlags.DemandMaster);
}
